<?php
namespace bbcsky\payment;

use Yii;
use yii\base\InvalidConfigException;
use yii\base\InvalidValueException;

class Alipay extends Payment
{
    //public $sign_type = 'RSA';
    public $input_charset = 'utf-8';
    //public $transport = 'http';
    public $service = 'mobile.securitypay.pay';
    public $service_refund = 'refund_fastpay_by_platform_pwd';
    public $gateway_refund = 'https://mapi.alipay.com/gateway.do';
    public $order_pre = 'Alipay';

    private $partner;
    private $key;
    private $key_path;
    private $ali_pub_path;
    private $notify_data;
    private $batch_trans_account;
    private $batch_trans_account_name;
    //private $ali_ca_path;
    private $cacert;
    //private $curl_proxy_host;
    //private $curl_proxy_port;

    public function setPartner($partner)
    {
        $this->partner = $partner;
    }

    public function setKey($key)
    {
        $this->key = $key;
    }

    public function setBatch_trans_account($batch_trans_account)
    {
        $this->batch_trans_account = $batch_trans_account;
    }

    public function setBatch_trans_account_name($batch_trans_account_name)
    {
        $this->batch_trans_account_name = $batch_trans_account_name;
    }

    public function setKey_path($key_path)
    {
        $this->key_path = \Yii::getAlias($key_path);
    }

    public function setAli_pub_path($ali_pub_path)
    {
        $this->ali_pub_path = \Yii::getAlias($ali_pub_path);
    }

    public function setCacert($cacert)
    {
        $this->cacert = \Yii::getAlias($cacert);
    }

    public function init()
    {
        parent::init();
        $needs = array('partner','key','key_path','ali_pub_path');
        foreach($needs as $need)
        {
            if(empty($this->{$need}))
            {
                throw new InvalidConfigException(get_class($this) . " must define alipay's params {$need}.");
            }
        }
    }

    /**
     * 支付接口
     * @param $order
     * @return mixed
     */
    public function pay($order)
    {

    }

    /**
     * 预支付接口，在APP上发起支付
     * @param $order
     * @return mixed
     */
    public function prepay($order)
    {
        $needs = array('title','order_sn','body','total_fee');
        foreach($needs as $need)
        {
            if(!isset($order[$need]))
            {
                throw new InvalidConfigException(get_class($this) . " \$order 中必须包含键 {$need}.");
            }
        }
        $paras = ['subject' => $order['title']];
        $paras['out_trade_no'] = $this->order_pre.$order['order_sn'];
        $paras['body'] = $order['body'];
        $paras['total_fee'] = round($order['total_fee'],2);
        if($paras['total_fee'] <= 0)
        {
            throw new InvalidValueException(get_class($this) . " 支付金额必须大于0");
        }
        $paras['notify_url'] = $this->notify_url;
        return $paras;
    }

    /**
     * @param 批量付款到支付宝接口
     */
    public function batchTrans($order)
    {
        $parameter = array(
            "service" => "batch_trans_notify",
            "partner" => $this->partner,
            "notify_url"	=> $this->notify_url,
            "email"	=> $this->batch_trans_account,
            "account_name"	=> $this->batch_trans_account_name,
            "pay_date"	=> $order['pay_date'],
            "batch_no"	=> $order['batch_no'],
            "batch_fee"	=> $order['batch_fee'],
            "batch_num"	=> $order['batch_num'],
            "detail_data"	=> $order['detail_data'],
            "sign_type"	=> 'MD5',
            "_input_charset"	=> $this->input_charset
        );
        $parameter['sign'] = $this->md5Sign($this->makeStr($parameter));
        return $this->buildRequestForm($parameter, 'GET', '确定');
    }

    /**
     * @param 批量付款到支付宝查询接口
     */
    public function btnStatusQuery($batch_no)
    {
        $parameter = array(
            "service" => "btn_status_query",
            "partner" => $this->partner,
            "email"	=> $this->batch_trans_account,
            "batch_no"	=> $batch_no,
            "sign_type"	=> 'MD5',
            "_input_charset"	=> $this->input_charset
        );
        $parameter['sign'] = $this->md5Sign($this->makeStr($parameter));
        //return $this->buildRequestHttp($parameter);
        $response = $this->buildRequestHttp($parameter);
        return $this->fromXml($response);
    }

    /**
     * 退款接口
     * @param $order
     * @return mixed
     */
    public function refund($order)
    {
        $time = explode(' ', microtime());
        $batch_no = date('Ymd', $time[1]) . substr($time[1], -5) . substr($time[0], 2, 6) . rand(10000, 99999);
        $batch_num = 0;
        $detail_data = '';
        if(!empty($order['serial_no']) && !empty($order['pay_amount']))
        {
            $batch_num = 1;
            $detail_data = $order['serial_no'] . '^' . $order['pay_amount'] . '^';
            if(!empty($order['remark']) && strlen($order['remark']) <= 100)
            {
                $detail_data .= $order['remark'];
            }
        }
        else
        {
            foreach($order as $o)
            {
                if(isset($o['serial_no']) && isset($o['pay_amount']))
                {
                    $batch_num ++;
                    $detail_data = $o['serial_no'] . '^' . $o['pay_amount'] . '^';
                    if(!empty($order['remark']) && strlen($order['remark']) <= 100)
                    {
                        $detail_data .= $order['remark'];
                    }
                    $detail_data .= '#';
                }
            }
            if(empty($detail_data))
            {
                throw new InvalidValueException(get_class($this) . " \$order 中必须包含键 serial_no,pay_amount或属于他们的数组.");
            }
            $detail_data = substr($detail_data,0,-1);
        }
        $paras = [
            'service'           =>$this->service_refund,
            'partner'           =>$this->partner,
            '_input_charset'    =>$this->input_charset,
            'sign_type'         =>'MD5',
            'notify_url'        =>$this->notify_url,
            //'seller_email'      =>'',
            'seller_user_id'    =>$this->partner,
            'refund_date'       =>date('Y-m-d H:i:s'),
            'batch_no'          =>$batch_no,
            'batch_num'         =>$batch_num,
            'detail_data'       =>$detail_data,
        ];
        $paras['sign'] = $this->md5Sign($this->makeStr($paras));
        return $this->buildRequestForm($paras, 'POST', 'submit');
    }

    public function checkNotify()
    {
        $paras = $_POST;
        if(strtoupper($paras['sign_type']) == 'MD5')
        {
            $sign = $this->md5Verify($this->makeStr($paras),$paras['sign']);
        }
        else
        {
            $sign = $this->rsaVerify($this->makeStr($paras),$paras['sign']);
        }
        if($sign)
        {
            return $paras;
        }
        throw new InvalidValueException(get_class($this) . " 验证签名失败.");
    }

    public function notify()
    {
        $paras = $_POST;
        if(strtoupper($paras['sign_type']) == 'MD5')
        {
            $sign = $this->md5Verify($this->makeStr($paras),$paras['sign']);
        }
        else
        {
            $sign = $this->rsaVerify($this->makeStr($paras),$paras['sign']);
        }
        if($sign)
        {
            $paras['trade_status'] = isset($paras['trade_status'])? trim($paras['trade_status']) : '';
            $paras['seller_id'] = isset($paras['seller_id'])? trim($paras['seller_id']) : '';
            if($paras['seller_id'] != $this->partner)
            {
                $paras['total_fee'] = 0;
            }
            if(!in_array($paras['trade_status'],['TRADE_SUCCESS','TRADE_FINISHED']))
            {
                $paras['total_fee'] = 0;
            }
            if(!empty($paras['refund_status']))
            {
                $paras['total_fee'] = 0;
            }
            $this->notify_data = $paras;
            return $paras;
        }
        else
        {
            throw new InvalidValueException(get_class($this) . " 验证签名失败.");
        }
    }

    /**
     * Notify处理完成接口
     * @return mixed
     */
    public function finish()
    {
        return 'success';
    }

    /**
     * 设置Notify回调接口
     * @return mixed
     */
    public function setNotifyUrl($url)
    {
        $this->notify_url = $url;
    }

    /**
     * 获得Notify返回的支付金额
     * @return mixed
     */
    public function getTotalFee($total_fee = null)
    {
        if($total_fee)
        {
            return round($total_fee,2,PHP_ROUND_HALF_DOWN);
        }
        if(isset($this->notify_data['total_fee']))
        {
            return round($this->notify_data['total_fee'],2,PHP_ROUND_HALF_DOWN);
        }
        return false;
    }

    /**
     * 获得Notify返回的交易号
     * @return mixed
     */
    public function getSerialNo($arr = null)
    {
        if(isset($arr['trade_no']))
        {
            return $arr['trade_no'];
        }
        if(isset($this->notify_data['trade_no']))
        {
            return $this->notify_data['trade_no'];
        }
        return false;
    }

    /**
     * 获得Notify返回的原始数据
     * @return mixed
     */
    public function getNotifyRaw()
    {
        return $_POST;
    }

    private function rsaSign($data)
    {
        $priKey = file_get_contents($this->key_path);
        $res = openssl_get_privatekey($priKey);
        openssl_sign($data, $sign, $res);
        openssl_free_key($res);
        //base64编码
        $sign = base64_encode($sign);
        return $sign;
    }

    private function rsaVerify($data, $sign)
    {
        $pubKey = file_get_contents($this->ali_pub_path);
        $res = openssl_get_publickey($pubKey);
        $result = (bool)openssl_verify($data, base64_decode($sign), $res);
        openssl_free_key($res);
        return $result;
    }

    private function rsaDecrypt($content)
    {
        $priKey = file_get_contents($this->key_path);
        $res = openssl_get_privatekey($priKey);
        //用base64将内容还原成二进制
        $content = base64_decode($content);
        //把需要解密的内容，按128位拆开解密
        $result  = '';
        for($i = 0; $i < strlen($content)/128; $i++  ) {
            $data = substr($content, $i * 128, 128);
            openssl_private_decrypt($data, $decrypt, $res);
            $result .= $decrypt;
        }
        openssl_free_key($res);
        return $result;
    }

    private function makeStr($paras)
    {
        ksort($paras);
        reset($paras);
        $app_str = '';
        foreach($paras as $key => $val)
        {
            if($key == "sign" || $key == "sign_type" || $val == "")
            {
                if($val == "")
                {
                    unset($paras[$key]);
                }
                continue;
            }
            $app_str .= $key.'='.$val.'&';
        }
        return substr($app_str,0,-1);
    }

    private function md5Sign($prestr)
    {
        if(get_magic_quotes_gpc()){
            $prestr = stripslashes($prestr);
        }
        $prestr = $prestr . $this->key;
        return md5($prestr);
    }

    private function md5Verify($prestr, $sign)
    {
        if(get_magic_quotes_gpc()){
            $prestr = stripslashes($prestr);
        }
        $prestr = $prestr . $this->key;
        $mysgin = md5($prestr);
        if($mysgin == $sign) {
            return true;
        }
        else {
            return false;
        }
    }

    private function buildRequestForm($para, $method, $button_name)
    {
        $sHtml = "<form id='alipaysubmit' name='alipaysubmit' action='".$this->gateway_refund."' method='".$method."'>";
        foreach($para as $key=>$val){
            $sHtml.= "<input type='hidden' name='".$key."' value='".$val."'/>";
        }
        $sHtml = $sHtml."<input type='submit' value='".$button_name."'></form>";
        $sHtml = $sHtml."<script>document.forms['alipaysubmit'].submit();</script>";
        return $sHtml;
    }

    private function buildRequestHttp($para)
    {
        return $this->getHttpResponsePOST($this->gateway_refund, $para);
    }

    /**
     * 远程获取数据，POST模式
     * 注意：
     * 1.使用Crul需要修改服务器中php.ini文件的设置，找到php_curl.dll去掉前面的";"就行了
     * 2.文件夹中cacert.pem是SSL证书请保证其路径有效，目前默认路径是：getcwd().'\\cacert.pem'
     * @param $url 指定URL完整路径地址
     * @param $cacert_url 指定当前工作目录绝对路径
     * @param $para 请求的数据
     * @param $input_charset 编码格式。默认值：空值
     * return 远程输出的数据
     */
    private function getHttpResponsePOST($url, $para)
    {
        //$url = $url."?_input_charset=".$this->input_charset;
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_CAINFO,$this->cacert);//证书地址
        curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl,CURLOPT_POST,true); // post传输数据
        curl_setopt($curl,CURLOPT_POSTFIELDS,$para);// post传输数据
        $responseText = curl_exec($curl);
        //var_dump( curl_error($curl) );//如果执行curl过程中出现异常，可打开此开关，以便查看异常内容
        curl_close($curl);

        return $responseText;
    }

    private function fromXml($xml)
    {
        if(!$xml){
            throw new InvalidValueException("xml数据异常！");
        }
        try
        {
            $values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        }
        catch(\Exception $e)
        {
            throw new InvalidValueException("xml数据异常！");
        }
        return $values;
    }
}